<?php

namespace Lib\Service;

use Lib\Base\Config;
use Lib\Base\Request;
use Lib\Base\Validate;
use Lib\Cache\RedisAction;
use Lib\Db\Query;

class WxBind
{
    private $key;
    private $delimiter;

    /**
     * 构造函数
     * @param string $skey awp中配置的业务代码。默认为空，自动取GET请求中的skey。
     */
    public function __construct(string $skey = '')
    {
        $skey = $skey == '' ? Request::param('skey') : $skey;
        if (Validate::checkBizCode($skey)) {
            $this->key = $skey;
        } else {
            $this->key = '';
        }

        $this->delimiter = '|';
    }

    /**
     * 根据openid生成连接上state参数的值
     * @param string $openid 账号ID
     * @return string state参数的值
     */
    public function getState(string $openid)
    {
        $key = Config::getFrameworkCfg('wechat_bind.secret', 'key');
        if ($key === false) {
            recordLog('从配置文件中读取生成微信绑定信息state的key失败！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }
        $state = $this->key . "," . $openid . "," . md5($this->key . $openid . $key) . "," . time();
        return $state;
    }

    /**
     * 根据公众号的openid查询对应的绑定信息
     * @param string $openid 账号ID
     * @return array
     * 有绑定信息：
     * [
     *      'iRet' => '0-查询正确，其他-查询出错',
     *      'sMsg' => '错误信息',
     *      'data' => [
     *          'sWechatOpenID' => '微信公众号对应的openid',
     *           'sGameOpenID' = '游戏业务appid对应的公众号',
     *           'nArea' => '1-微信，2-手Q',
     *           'nType' => '1-Android(安卓)，2-IOS(苹果)', 
     *           'dtUpdate' => '最后更新绑定信息的时间'
     *           'sOptionalUin' => 'QQ号',
     *           'sOptionalSubArea' => '区服ID',
     *           'sOptionalExtra' => '其他扩展信息（例如sRoleId）'
     *      ]
     * ]
     * 无绑定信息：
     * [
     *      'iRet' => '0',
     *      'sMsg' => 'success',
     *      'data' => []
     * ]
     */
    public function getByWechat(string $wxOpenid)
    {
        return $this->getBindData($wxOpenid, 'wechat');
    }

    /**
     * 根据公游戏的openid查询对应的绑定信息
     * @param string $openid 账号ID
     * @return array
     * 有绑定信息：
     * [
     *      'iRet' => '0-查询正确，其他-查询出错',
     *      'sMsg' => '错误信息',
     *      'data' => [
     *          'sWechatOpenID' => '微信公众号对应的openid',
     *           'sGameOpenID' = '游戏业务appid对应的公众号',
     *           'nArea' => '1-微信，2-手Q',
     *           'nType' => '1-Android(安卓)，2-IOS(苹果)', 
     *           'dtUpdate' => '最后更新绑定信息的时间'
     *           'sOptionalUin' => 'QQ号',
     *           'sOptionalSubArea' => '区服ID',
     *           'sOptionalExtra' => '其他扩展信息（例如sRoleId）'
     *      ]
     * ]
     * 无绑定信息：
     * [
     *      'iRet' => '0',
     *      'sMsg' => 'success',
     *      'data' => []
     * ]
     */
    public function getByGame(string $gameOpenid)
    {
        return $this->getBindData($gameOpenid, 'game');
    }

    /**
     * 更新绑定信息
     * @param array 要更新的字段信息
     * [
     *          'sWechatOpenID' => '微信公众号对应的openid',
     *           'sGameOpenID' = '游戏业务appid对应的公众号',
     *           'nArea' => '1-微信，2-手Q',
     *           'nType' => '1-Android(安卓)，2-IOS(苹果)',
     *           'sOptionalUin' => 'QQ号',
     *           'sOptionalSubArea' => '区服ID',
     *           'sOptionalExtra' => '其他扩展信息（例如sRoleId）'
     * ]
     * @return array
     * [
     *      'iRet' => '0-成功，其他-失败',
     *      'sMsg' => '错误消息'
     * ]
     */
    public function updateBindInfo(array $data)
    {
        if (!Validate::checkOpenid($data['sWechatOpenID'])) {
            return ['iRet' => '-1', 'sMsg' => '微信openid不合法！'];
        }
        if (!Validate::checkOpenid($data['sGameOpenID'])) {
            return ['iRet' => '-2', 'sMsg' => '游戏openid不合法！'];
        }

        if (!in_array($data['nArea'], ['1', '2'])) {
            return ['iRet' => '-3', 'sMsg' => '渠道ID错误！'];
        }

        if (!in_array($data['nType'], ['1', '2'])) {
            return ['iRet' => '-4', 'sMsg' => '系统ID错误！'];
        }

        if (isset($data['sOptionalUin']) && !Validate::checkQQ($data['sOptionalUin'])) {
            return ['iRet' => '-5', 'sMsg' => 'QQ号错误！'];
        }

        if (isset($data['sOptionalSubArea']) && !Validate::checkPartition($data['sOptionalSubArea'])) {
            return ['iRet' => '-6', 'sMsg' => '区服ID错误！'];
        }

        if (isset($data['sOptionalExtra']) && !Validate::checkRoleId($data['sOptionalExtra'])) {
            return ['iRet' => '-7', 'sMsg' => '角色ID错误！'];
        }

        //先删Redis，这里需要注意，微信公众号对应的openid在表中是唯一主键，但是与之绑定的游戏openid在表中只是索引，并非唯一
        //例如：wxopenid1先绑定了gameopenid1,然后换绑到gameopenid2。此时就需要将wxopenid1,gameopenid1,gameopenid2以及与之对应的wxopenid2对应的缓存全部删除
        $delOpenids = [];
        $oldData = $this->getDataFromCache($data['sWechatOpenID']);
        if ($oldData) {
            $delOpenids[] = $oldData['sWechatOpenID'];
            $delOpenids[] = $oldData['sGameOpenID'];
        }
        $oldData = $this->getDataFromCache($data['sGameOpenID']);
        if ($oldData) {
            $delOpenids[] = $oldData['sWechatOpenID'];
            $delOpenids[] = $oldData['sGameOpenID'];
        }
        if (!empty($delOpenids)) {
            $delOpenids = array_unique($delOpenids);
            $delNum = $this->delDataFromCache($delOpenids);
            if ($delNum != count($delOpenids)) {
                recordLog('从Redis中删除账号的绑定信息失败！成功删除数：' . $delNum . ',实际应该删除数量：' . count($delOpenids) . ',需要删除的openid：' . var_export($delOpenids, true), __FILE__, __LINE__, LP_ERROR);
                return ['iRet' => '-7', 'sMsg' => '系统繁忙，请稍后再试！'];
            }
        }

        //再更新db
        $dbCon = $this->getDbInfo();
        $query = new Query($dbCon);
        $query->transBegin();
        $query->table('tbBindData')->where(['sGameOpenID' => $data['sGameOpenID']])->delete();
        if (isset($data['sOptionalUin']) || isset($data['sOptionalSubArea']) || isset($data['sOptionalExtra'])) {
            $replaceData = [
                'sWechatOpenID' => $data['sWechatOpenID'],
                'sGameOpenID' => $data['sGameOpenID'],
                'nArea' => $data['nArea'],
                'nType' => $data['nType'],
                'dtUpdate' => date('Y-m-d H:i:s'),
                'sOptionalUin' => $data['sOptionalUin'],
                'sOptionalSubArea' => $data['sOptionalSubArea'],
                'sOptionalExtra' => $data['sOptionalExtra']
            ];
        } else {
            $replaceData = [
                'sWechatOpenID' => $data['sWechatOpenID'],
                'sGameOpenID' => $data['sGameOpenID'],
                'nArea' => $data['nArea'],
                'nType' => $data['nType'],
                'dtUpdate' => date('Y-m-d H:i:s')
            ];
        }
        $query->table('tbBindData')->replace($replaceData);
        $ret = $query->transCommit();
        return $ret ? ['iRet' => '0', 'sMsg' => 'success'] : ['iRet' => '-8', 'sMsg' => '系统繁忙，请稍后再试！'];
    }

    /**
     * 生成微信账号绑定链接
     * @param string $wxUrl 微信账号绑定活动页面
     * @param bool|string 成功返回绑定url，失败返回false
     */
    public function getWeixinBindUrl(string $wxUrl)
    {
        $openid = Request::param('sOpenID');    //关注的微信公众号对应openid，由awp透传

        if (!Validate::checkOpenid($openid)) {
            recordLog('生成微信账号绑定链接的openid错误！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }
        if (!Validate::checkQQUrl($wxUrl)) {
            recordLog('生成微信账号绑定链接的url错误！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }

        $appid = 'wx1cd4fbe9335888fe';
        $state = $this->getState($openid);
        $homeUrl = urlencode($wxUrl . '?acctype=wx&appid=' . $appid);
        $redirect_uri = urlencode("https://iu.qq.com/wxauth/redirect.html?url=" . $homeUrl);
        $url = "https://open.weixin.qq.com/connect/oauth2/authorize?";
        $url .= "appid={$appid}";
        $url .= "&redirect_uri={$redirect_uri}";
        $url .= "&response_type=code&scope=snsapi_base";
        $url .= "&state=" . $state;
        $url .= "#wechat_redirect";
        return $url;
    }

    /**
     * 生成QQ账号绑定链接
     * @param string $qqUrl QQ账号绑定活动页面
     * @param bool|string 成功返回绑定url，失败返回false
     */
    public function getQQBindUrl(string $qqUrl)
    {
        $openid = Request::param('sOpenID');    //关注的微信公众号对应openid，由awp透传

        if (!Validate::checkOpenid($openid)) {
            recordLog('生成微信账号绑定链接的openid错误！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }
        if (!Validate::checkQQUrl($qqUrl)) {
            recordLog('生成微信账号绑定链接的url错误！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }

        $state = $this->getState($openid);
        return $qqUrl . "?state=" . $state;
    }

    /**
     * 生成微信/QQ账号带绑定信息的签到链接
     * @param string $url 签到活动连接
     * @param string $appid 游戏业务的appid
     * @param array $bindInfo 绑定信息，调用getByWechat()函数获得的数据
     * @param bool|string 成功返回带绑定信息签到url，失败返回false
     */
    public function getSignUrl(string $url, string $appid, array $bindInfo)
    {
        $state = $this->GetState($bindInfo['sWechatOpenID']);
        $url = $url . "?state=" . $state . "&sArea=" . $bindInfo['nArea'] . "&sPartition=" . $bindInfo['sOptionalSubArea']  . "&sPlatId=" . $bindInfo['nType']  . "&sRoleId=" . $bindInfo['sOptionalExtra']  . "&appid=" . $appid;
        return $url;
    }

    /**
     * 根据openid查询对应的绑定信息
     * @param string $openid 账号ID
     * @param string $type 账号类型 wechat|game
     * @return array 
     * [
     *      'iRet' => '0-查询正确，其他-查询出错',
     *      'sMsg' => '错误信息',
     *      'data' => [
     *          'sWechatOpenID' => '微信公众号对应的openid',
     *           'sGameOpenID' = '游戏业务appid对应的公众号',
     *           'nArea' => '1-微信，2-手Q',
     *           'nType' => '1-Android(安卓)，2-IOS(苹果)', 
     *           'dtUpdate' => '最后更新绑定信息的时间'
     *           'sOptionalUin' => 'QQ号',
     *           'sOptionalSubArea' => '区服ID',
     *           'sOptionalExtra' => '其他扩展信息（例如sRoleId）'
     *      ]
     * ]
     */
    private function getBindData(string $openid, string $type)
    {
        if (!Validate::checkOpenid($openid)) {
            return ['iRet' => '-1', 'sMsg' => 'openid错误！', 'data' => []];
        }
        $data = $this->getDataFromCache($openid);
        if ($data === false) {
            $data = $this->getDataFromDb($openid, $type);
            if ($data === false) {
                return ['iRet' => '-2', 'sMsg' => '系统繁忙，请稍后再试！', 'data' => []];
            }
            if (empty($data)) {
                return ['iRet' => '0', 'sMsg' => 'success', 'data' => []];
            }
            //将数据更新到Redis缓存
            $this->updateData2Cache($openid, $data);
        }
        return ['iRet' => '0', 'sMsg' => 'success', 'data' => $data];
    }

    private function getDataFromDb(string $openid, string $type)
    {
        $dbCon = $this->getDbInfo();
        if ($dbCon === false) {
            return false;
        }
        $query = new Query($dbCon);
        $where = $type == 'wechat' ? ['sWechatOpenID' => $openid] : ['sGameOpenID' => $openid];
        $bindData = $query->table('tbBindData')->where($where)->select();
        if ($bindData === false) {
            $errorInfo = $query->getError();
            recordLog('db查询异常，error:' . var_export($errorInfo, true), __FILE__, __LINE__, LP_ERROR);
            return false;
        }
        return $bindData['rows'] ? $bindData['data'][0] : [];
    }

    private function getRedisKey(string $openid)
    {
        return 'AWP_' . strtoupper($this->key) . '_' . $openid;
    }

    private function updateData2Cache(string $openid, array $data)
    {
        $config = Config::getFrameworkCfg('wechat_bind_redis.cfg');
        if ($config === false) {
            recordLog('读取微信绑定信息存储的redis连接信息失败！');
            return false;
        }
        $config['prefix'] = 0;
        $redis = new RedisAction($config);
        $key = $this->getRedisKey($openid);
        $dataStr = implode($this->delimiter, $data);
        return $redis->setEx($key, 30 * 86400, $dataStr);   //key的有效期统一设置为1个月
    }

    private function getDataFromCache(string $openid)
    {
        $bindData = [];
        $bindKey = [
            'sWechatOpenID',
            'sGameOpenID',
            'nArea',
            'nType',
            'dtUpdate',
            'sOptionalUin',
            'sOptionalSubArea',
            'sOptionalExtra',
        ];

        $config = Config::getFrameworkCfg('wechat_bind_redis.cfg');
        if ($config === false) {
            recordLog('读取微信绑定信息存储的redis连接信息失败！');
            return false;
        }
        $config['prefix'] = 0;
        $redis = new RedisAction($config);
        $key = $this->getRedisKey($openid);
        $bindStr = $redis->get($key);
        if ($bindStr === false) {
            return false;
        }
        $bindArr = explode($this->delimiter, $bindStr);
        foreach ($bindKey as $index => $filed) {
            $bindData[$filed] = $bindArr[$index] ?? '';
        }
        $bindData['sOptionalUin'] = $bindData['sOptionalUin'] == '' ? 0 : $bindData['sOptionalUin'];
        return $bindData;
    }

    private function delDataFromCache(array $openids)
    {
        $delKeys = [];
        $config = Config::getFrameworkCfg('wechat_bind_redis.cfg');
        if ($config === false) {
            recordLog('读取微信绑定信息存储的redis连接信息失败！');
            return false;
        }
        $config['prefix'] = 0;
        $redis = new RedisAction($config);
        foreach ($openids as $val) {
            $delKeys[] = $this->getRedisKey($val);
        }
        return $redis->del($delKeys);
    }

    private function getDbInfo()
    {
        $sNode = "DB_" . strtoupper($this->key);
        if (!file_exists(WECHAT_BIND_DB_CFG)) {
            recordLog(WECHAT_BIND_DB_CFG . '文件不存在！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }

        $config = parse_ini_file(WECHAT_BIND_DB_CFG, true);
        if ($config === false) {
            recordLog(WECHAT_BIND_DB_CFG . '文件解释失败！', __FILE__, __LINE__, LP_ERROR);
            return false;
        }
        /* 如果没有binder_host，则在默认的host_node节点下*/
        $sDBNode = $config[$sNode]["binder_host"] ?? $config[$sNode]["host_node"];


        /* 如果没有binder_db，则在默认的proxy_db节点下*/
        $sDBName = $config[$sNode]["binder_db"] ?? $config[$sNode]["proxy_db"];

        return ['DB_GROUP_0' => [
            'databaseName' => $sDBName, //数据库名称
            'instanceName' => $sDBNode, //节点名称，根据实际情况填写，如果多个节点，不同节点之间用|隔开，只有一个不用加|
        ]];
    }
}
